iT邦幫忙

2023 iThome 鐵人賽

DAY 11
0
自我挑戰組

Rayeee 的 TypeScript 的學習日記系列 第 11

<20230912> Day11. 初探探 Interfaces

  • 分享至 

  • xImage
  •  

在 TypeScript 中,最重要的就是 Interfaces 和 Class 的協作,Interfaces 和 Class 的相互作用,是我們在編寫 TypeScript 時真正獲得重用性的原因 -b

今日大綱:
今天會先從為甚麼要用 Interfaces 開始舉例說起
然後再舉例說明一些比較正確使用 Interfaces 的情境

Interfaces 翻成介面 or 接口,他其實跟 number, string, boolean 一樣,就是一種型別(Type),只是他是用來描述物件的 屬性名稱值的類型

今天就來記錄一下,甚麼是 Interfaces

先看以下範例

const monster = {
	name: 'godzilla',
	year: 1954,
	canFly: true,
}

const printMonster = (monster: {name: string, year: number, canFly: boolean}) => {
	console.log(`
		name = ${monster.name}
		born in ${monster.year}
		can fly? ${monster.canFly}
	`)
}

雖然程式碼沒有問題,但是 printMonster 的接收的參數 monster 的 Type Annotations 真的太長了,導致程式碼難以閱讀

而且如果現在有其他方法也要接收一樣的參數,那該方法也要在寫一次一樣的 Type Annotations,簡直是場災難

const monster = {
	name: 'godzilla',
	year: 1954,
	canFly: true,
}

const printMonster = (monster: {name: string, year: number, canFly: boolean}) => {
	console.log(`
		name = ${monster.name}
		born in ${monster.year}
		can fly? ${monster.canFly}
	`)
}

const printMonsterStory = (monster: {name: string, year: number, canFly: boolean}) => {
	console.log(`It's name is = ${monster.name}`)
}

這時我們就可以使用更好的解決方法 => Interfaces

建立一個 Interfaces,我們可以想像就像建立一個新型別,就像 number, string 那樣的型別

TypeScript 會像檢查其他型別一樣,檢查我們定義好的這個值有沒有符合 interface

以下範例

interface Monster {
	name: string;
	year: number;
	canFly: boolean;
}

const monsterGodzilla: Monster = {
	name: 'godzilla',
	year: 1954,
	canFly: true,
}

像這樣我們定義好一個 interface 接口,並在宣告物件 monsterGodzilla 時註記要符合 interface Monster,這樣子 TypeScript 就會檢查 monsterGodzilla 是否符合 interface Monster

確認裡面的 屬性名稱值的類型 是否正確,如果有錯了就會報錯

https://ithelp.ithome.com.tw/upload/images/20230912/20162544Hx2prZApkX.png

Interface 中定義屬性的 Type,不僅僅只侷限在一般基本的 Type ,所以的 Type 都可以,像是 Date 跟 function 都可以

interface Monster {
	name: string;
	year: Date;
	canFly: boolean;
	attack(): string;
}

但是在使用 interface 的時候也要注意,有時候有些錯誤不會在程式馬上出現,而是語意上的錯誤,這類錯誤容易造成未來 Type 上有錯誤

來看以下程式碼

// 怪獸的 interface
interface Monster {
	name: string;
	year: number;
	canFly: boolean;
	attack(): string;
}

// 現在有的一個怪獸物件
const godMonster = {
	name: 'godzilla',
	year: 1954,
	canFly: true,
    attack(): string {
        return `${this.name} attack!!!`
    }
}

// - 現在有的其一方法,傳入參數 Type 的定義為 Monster
// - 語意為: 要符合怪獸條件的才可傳入,TypeScript 會檢查
// - printMonsterAttack 命名的語意為印出怪獸
const printMonsterAttack = (monster: Monster) => {
    console.log(monster.attack())
}

printMonsterAttack(godMonster)

這段看起來沒問題的程式碼其實藏有一些語意上的錯誤、Type 的錯誤,以及可以優化的地方

下面就來看看如何優化

1. 現在的方法 printMonsterAttack 命名的語意為印出怪獸攻擊,且接收參數須符合怪獸的 interface

但是這個方法只有用到 Monster interface 的 attack() 方法,這樣子我們還可以說,一個怪獸一定要符合 Monster interface 的所有特性嗎?

答案是 NO ,可以看下面這段,如果我們把 interface 上的一些特性刪掉,並不會跳任何錯誤

https://ithelp.ithome.com.tw/upload/images/20230912/201625440ziRxk1ME5.png

為甚麼呢?

我們這個寫法,當我們調用 printMonsterAttack() 的時候,傳入的參數需要符合 Monster interface,我們傳入 godMonster,TypeScript 在背後幫我們做了一次快速檢查,這個 godMonster 是否符合 Monster interface

檢查內容就是檢查 godMonster 裡面是否有一個 attack() 參數,並且是回傳 string 的。
實際檢查 godMonster 內部發現 OK 這裡面確實有 attack(),並且回傳的也是 string。

然後... 就沒有然後了
這是 TypeScript 唯一會檢查的問題。

其他 godMonster 裡面有其他參數,TS 都不會在意
不會說,哦哦 你有其他參數,你不能稱為怪獸

他只會去檢查 interface 的中的屬性列表是否符合

2. 還有一個問題是現在 interface Monster 的取名是否合適呢?

因為 interface 裡面只有檢查是否有 attack(): string 參數,所以 interface 的名稱改成這樣更加合理 Attackable

https://ithelp.ithome.com.tw/upload/images/20230912/20162544DWLQijLn3X.png

這樣寫的話,就可以說,要被認為是可攻擊類型 (Attackable Type) 的話,你必須要有一個名稱為 attack() 的參數,並且返回值要是 string。

現在這樣寫,對 interface Attackable 的定義精簡了很多,但是通用性更高了

3. 關於 printMonsterAttack() 的取名

printMonsterAttack() 的取名現在看起來,跟怪獸有什麼關係呢?
他只是印出怪獸的攻擊

這個函式現在做的事情是檢查參數有可以 attack() 的方法,然後印出攻擊名稱,所以其實跟怪獸沒有關係,我們的語意也可以改變,函式的名稱和參數名稱都可以修改一下

https://ithelp.ithome.com.tw/upload/images/20230912/20162544UgxMbOjGHJ.png

這樣就調整完一些語意不合的部分,之後會再提到為甚麼需要這樣『正確』的語意呢?

重用程式碼 Code Reuse with Interface

現在我們來試著加入完全不同的物件 player

interface Attackable {
	attack(): string;
}

const godMonster = {    // 怪獸物件
	name: 'godzilla',
	year: 1954,
	canFly: true,
    attack(): string {
        return `${this.name} attack!!!`
    }
}

const player1 = {    // 新加的不同物件
    name: 'Victor',
    hp: 100,
    skill: 'fire ball',
    attack(): string {
        return `${this.name} use ${this.skill}!!`
    }
}

const printAttack = (target: Attackable) => {
    console.log(target.attack()) 
}

printAttack(godMonster)
printAttack(player1)  // 可以正確被傳入 printAttack()

看出來了嗎?這就是 interface 的意義

這兩個代表非常不同的物件,但是他們都有一個共同的方法 printAttack(),這代表他們都被視為符合 Attackable Type,所以這兩個物件都可以被傳入 printAttack() 中使用


上面這個重點是,我們可以使用同一個 Interface 來描述非常不同的兩個物件
通過這樣實作,我們可以讓這些不同的物件共用一個 Interface、使用同一個方法 printAttack(),並且符合語意

我們現在在應用程序中獲得了一個可重用性更高的函式 printAttack

const printAttack = (target: Attackable) => {
    console.log(target.attack()) 
}

這個方法可以滿足任何滿足 type 符合 Attackable 的物件
這個範例雖然十分做作,但是可以初步了解 Interface 在 typescript 中的意義。

參考資料


上一篇
<20230911> 再...再吸口... 再吸一口 Enum
下一篇
<20230913> Day12. Interfaces 再探探
系列文
Rayeee 的 TypeScript 的學習日記30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言